/*
* Copyright (c) 2012 Kyle Hayward
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#pragma once

#include "Vector.h"

namespace OptixDotNet 
{
	ref class Program;
	ref class Variable;
	ref class Buffer;
	ref class Geometry;

	/// <summary>
	/// Delegate used to call user functions every N seconds.
	/// </summary>
	public delegate int OptixTimeoutHandler();

	/// <summary>
	/// Represents a OptiX capable GPU device.
	/// </summary>
	public value class Device
	{
	public:
		/// <summary>
		/// Index id of the device.
		/// </summary>
		int Index;

		/// <summary>
		/// Maximum threads per block.
		/// </summary>
		int MaxThreadsPerBlock;	

		/// <summary>
		/// GPU clock rate.
		/// </summary>
		int ClockRate;

		/// <summary>
		/// Streaming muli-processor count of the device.
		/// </summary>
		int ProcessorCount;

		/// <summary>
		/// Is execution timeout enabled.
		/// </summary>
		int ExecutionTimeOutEnabled;

		/// <summary>
		/// Maximum number of textures that can be created.
		/// </summary>
		int MaxHardwareTextureCount;

		/// <summary>
		/// Name of the device.
		/// </summary>
		System::String^ Name;

		/// <summary>
		/// Compute capability of the device.
		/// </summary>
		OptixDotNet::Math::Int2 ComputeCapability;

		/// <summary>
		/// Total memory of the device.
		/// </summary>
		int TotalMemory;
	};

	/// <summary>
	/// Instance of a running OptiX engine. Provides an interface for creating and launching the ray-tracing engine.
	/// </summary>
	public ref class Context : public IVariableContainer
	{		
	public:

		/// <summary>
		/// Creates an OptiX context.
		/// </summary>
		Context();
		~Context();
		!Context();

		/// <summary>
		/// Validates the current context. This will check the programs, buffers, and variables set to make sure everything links correctly.
        /// </summary>
		void Validate() { CheckError( rtContextValidate( mContext ) ); }

		/// <summary>
		/// Destroys the internal Optix resource. This also handles the destruction of all other created Optix resources.
        /// </summary>
		void Destroy()
		{
			if( mContext )
				CheckError( rtContextDestroy( mContext ) );
			mContext = NULL;
		}

		/// <summary>
		/// Compiles the context. This will validate, compile programs, and build an acceleration structure.
        /// </summary>
		void Compile();

		/// <summary>
		/// Builds the acceleration tree for the geometry hierarchy. Internally uses a call to rtContextLaunch3D( 0, 0, 0, 0 ).
		/// This controls where the acceleration building happens. Otherwise the accel tree is built on the next optix launch.
        /// </summary>
		void BuildAccelTree() { CheckError( rtContextLaunch3D( mContext, 0, 0, 0, 0 ) ); }

		/// <summary>
		/// Performs a 1D Optix Launch, using the entry point program at entryPointIndex
        /// </summary>
		void Launch( unsigned int entryPointIndex, Size width ) 
		{ 
			CheckError( rtContextLaunch1D( mContext, entryPointIndex, width ) ); 
		}

		/// <summary>
		/// Performs a 2D Optix Launch, using the entry point program at entryPointIndex
        /// </summary>
		void Launch( unsigned int entryPointIndex, Size width, Size height )
		{
			CheckError( rtContextLaunch2D( mContext, entryPointIndex, width, height ) );
		}

		/// <summary>
		/// Performs a 3D Optix Launch, using the entry point program at entryPointIndex
        /// </summary>
		void Launch( unsigned int entryPointIndex, Size width, Size height, Size depth ) 
		{ 
			CheckError( rtContextLaunch3D( mContext, entryPointIndex, width, height, depth ) ); 
		}

		/// <summary>
		/// Sets the ray generation program at entry point index [index] on the optix context. Ray generation programs will trace the initial ray (e.g. the eye ray for a camera)
        /// </summary>
        /// <param name="entryPointIndex">Entry point index of the ray generation program</param>
		/// <param name="program">Ray generation program</param>
		void SetRayGenerationProgram( int entryPointIndex, Program^ program );

		/// <summary>
		/// Sets the ray miss program at entry point index [index] on the optix context. Miss programs handle what happens when rays do not intersect any geometry
		/// There are a 1:1 ratio of miss programs and ray types.
        /// </summary>
        /// <param name="rayTypeIndex">Ray type index of the ray miss program</param>
		/// <param name="program">Ray generation program</param>
		void SetRayMissProgram( int rayTypeIndex, Program^ program );

		/// <summary>
		/// Sets the ray generation program at entry point index [index] on the optix context. Exception programs handle exceptions during a context launch
        /// </summary>
        /// <param name="entryPointIndex">Entry point index of the exception program</param>
		/// <param name="program">Exception program</param>
		void SetExceptionProgram( int entryPointIndex, Program^ program );

		/// <summary>
		/// Gets the OptiX device number associated with the name of a D3D9 adapter.
		/// </summary>
		/// <param name="adapterName">Name of a D3D9 adapter</param>
		int GetD3D9DeviceFromAdapter( System::String^ adapterName )
		{
			const char* str = StrToAnsii( adapterName );
			int device;
			CheckError( rtDeviceGetD3D9Device( &device, str ) );

			return device;
		}

		/// <summary>
		/// Gets the OptiX device number associated with the pointer to a D3D10 adapter.
		/// </summary>
		/// <param name="adapter">Pointer to a D3D10 adapter</param>
		int GetD3D10DeviceFromAdapter( System::IntPtr adapter )
		{
			int device;
			CheckError( rtDeviceGetD3D10Device( &device, static_cast<IDXGIAdapter*>( adapter.ToPointer() ) ) );
			return device;
		}

		/// <summary>
		/// Gets the OptiX device number associated with the pointer to a D3D11 adapter.
		/// </summary>
		/// <param name="adapter">Pointer to a D3D11 adapter</param>
		int GetD3D11DeviceFromAdapter( System::IntPtr adapter )
		{
			int device;
			CheckError( rtDeviceGetD3D11Device( &device, static_cast<IDXGIAdapter*>( adapter.ToPointer() ) ) );
			return device;
		}

		/// <summary>
		/// Gets the OptiX device number associated with the specified GL GPU.
		/// </summary>
		/// <param name="gpu">A handle to a GPU as returned from the WGL NV gpu affinity OpenGL extension.</param>
		int GetWGLDevice( System::IntPtr gpu )
		{
			int device;
			CheckError( rtDeviceGetWGLDevice( &device, gpu.ToPointer() ) );
			return device;
		}

		/// <summary>
		/// Create clustered triangle mesh for good memory coherence with paging on.
		/// Vertex, index and material buffers are created and attached to the mesh.
		/// Cluster's bounding box and intersection programs are attached to the mesh.
		/// The intersection program has the following attributes: 
		///   rtDeclareVariable(float3, texcoord, attribute texcoord, ); It is always zero
		///   rtDeclareVariable(float3, geometric_normal, attribute geometric_normal, ); 
		///   rtDeclareVariable(float3, shading_normal, attribute shading_normal, ); It is equal to geometric_normal
		/// 
		/// Created RTgeometry mesh expects there to be placed into a RTgeometryinstance where
		/// the mat_indices specified map into materials attached to the RTgeometryinstance
        /// </summary>
		/// <param name="usePTX32InHost64">Use 32bit PTX bounding box and intersection programs in 64bit application. Takes effect only with 64bit host.</param>
		/// <param name="numTris">Number of triangles in the mesh.</param>
		/// <param name="vertices">Array of vertex elements float3[x y z]</param>
		/// <param name="indices">Array of triangle indices [NumTris * 3]</param>
		/// <param name="materialIndices">Array of material indices = NumTris</param>
		generic< typename V >
		Geometry^ CreateClusteredMesh(	unsigned int usePTX32InHost64, 
										unsigned int numTris,
										array<V>^ vertices,
										array<unsigned int>^ indices,
										array<unsigned int>^ materialIndices );

		/// <summary>
		/// Create clustered triangle mesh for good memory coherence with paging on.
		/// Buffers for vertices, indices, normals, indices of normals,
		/// texture coordinates, indices of texture coordinates and materials are created and attached to the mesh.
		/// Cluster's bounding box and intersection programs are attached to the mesh.
		/// The intersection program has the following attributes: 
		///   rtDeclareVariable(float3, texcoord, attribute texcoord, ); 
		///   rtDeclareVariable(float3, geometric_normal, attribute geometric_normal, ); 
		///   rtDeclareVariable(float3, shading_normal, attribute shading_normal, ); 
		/// 
		/// Created RTgeometry mesh expects there to be placed into a RTgeometryinstance where
		/// the mat_indices specified map into materials attached to the RTgeometryinstance
		/// 
		/// Vertex, normal and texture coordinate buffers can be shared between many geometry objects
        /// </summary>
		/// <param name="usePTX32InHost64">Use 32bit PTX bounding box and intersection programs in 64bit application. Takes effect only with 64bit host.</param>
		/// <param name="numTris">Number of triangles in the mesh.</param>
		/// <param name="vertices">Array of vertex elements float3[x y z]</param>
		/// <param name="normals">Float3 Array of vertex normals float3[x y z]</param>
		/// <param name="texcoords">Float2 buffer of vertex normals float2[x y z]</param>
		/// <param name="indices">Array of triangle indices [NumTris * 3]</param>
		/// <param name="normalIndices">Array of vertex normals indices [NumTris * 3]</param>
		/// <param name="texcoordIndices">Array of texcoord indices [NumTris * 3]</param>
		/// <param name="materialIndices">Array of material indices = NumTris</param>
		generic< typename V >
		Geometry^ CreateClusteredMesh(	unsigned int usePTX32InHost64, 
										unsigned int numTris,
										array<V>^ vertices,
										Buffer^ normals,
										Buffer^ texcoords,
										array<unsigned int>^ indices,
										array<unsigned int>^ normalIndices,
										array<unsigned int>^ texcoordIndices,
										array<unsigned int>^ materialIndices );

		/// <summary>
		/// Sets the delegate that will be called during any long running Optix calls, such as Compile. Optix will internally call
		/// the delegate every minPollingSeconds. If the callback returns 1, the Optix API will abort the function.
        /// </summary>
		/// <param name="optixTimeOut">Delegate function that will be called every [minPollingSeconds]</param>
		/// <param name="minPollingSeconds">Amount of time Optix will wait before calling [optixTimeOut]</param>
		void SetOptixtTimeOutHandler( OptixTimeoutHandler^ optixTimeOut, double minPollingSeconds )
		{
			RTtimeoutcallback funcPtr = (RTtimeoutcallback)System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate( optixTimeOut ).ToPointer();
			CheckError( rtContextSetTimeoutCallback( mContext, funcPtr, minPollingSeconds ) );
		}

		/// <summary>
		/// Binds a D3D9 device to the context and enables d3d interop.
        /// </summary>
		property System::IntPtr D3D9Device
		{
			void set( System::IntPtr device )
			{
				mD3D9Device = device;
				CheckError( rtContextSetD3D9Device( mContext, static_cast<IDirect3DDevice9*>( device.ToPointer() ) ) );
			}
			inline System::IntPtr get()
			{
				return mD3D9Device;
			}
		}

		/// <summary>
		/// Binds a D3D10 device to the context and enables d3d interop.
        /// </summary>
		property System::IntPtr D3D10Device
		{
			void set( System::IntPtr device )
			{
				mD3D10Device = device;
				CheckError( rtContextSetD3D10Device( mContext, static_cast<ID3D10Device*>( device.ToPointer() ) ) );
			}
			inline System::IntPtr get()
			{
				return mD3D10Device;
			}
		}

		/// <summary>
		/// Binds a D3D11 device to the context and enables d3d interop.
        /// </summary>
		property System::IntPtr D3D11Device
		{
			void set( System::IntPtr device )
			{
				mD3D11Device = device;
				CheckError( rtContextSetD3D11Device( mContext, static_cast<ID3D11Device*>( device.ToPointer() ) ) );
			}
			inline System::IntPtr get()
			{
				return mD3D11Device;
			}
		}

		/// <summary>
		/// Gets the current Optix library version.
        /// </summary>
		property unsigned int OptixVersion
		{
			unsigned int get()
			{
				unsigned int version;
				CheckError( rtGetVersion( &version ) );

				return version;
			}
		}

		/// <summary>
		/// Gets or sets the devices currently being used by Optix
        /// </summary>
		property array<Device>^ Devices
		{
			array<Device>^ get()
			{
				array<int>^ indices = gcnew array<int>( DeviceCount );
				pin_ptr<int> devicePtr = &indices[0];

				CheckError( rtContextGetDevices( mContext, devicePtr ) );

				array<Device>^ devices = gcnew array<Device>( DeviceCount );
				for( unsigned int i = 0; i < DeviceCount; i++ )
				{
					Device device;
					device.Index = indices[i];
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_MAX_THREADS_PER_BLOCK,		sizeof( int ),						&device.MaxThreadsPerBlock ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_CLOCK_RATE,					sizeof( int ),						&device.ClockRate ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,			sizeof( int ),						&device.ProcessorCount ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_EXECUTION_TIMEOUT_ENABLED,	sizeof( int ),						&device.ExecutionTimeOutEnabled ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_MAX_HARDWARE_TEXTURE_COUNT,	sizeof( int ),						&device.MaxHardwareTextureCount ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_TOTAL_MEMORY,					sizeof( RTsize ),					&device.TotalMemory ) );
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY,			sizeof( OptixDotNet::Math::Int2 ),	&device.ComputeCapability ) );

					char name[256];
					CheckError( rtDeviceGetAttribute( indices[i], RT_DEVICE_ATTRIBUTE_NAME,							256,								(void*)name ) );
					device.Name = AnsiiToStr( name );

					devices[i] = device;
				}
				return devices;
			}
			void set( array<Device>^ devices )
			{
				array<int>^ indices = gcnew array<int>( devices->Length );
				for( int i = 0; i < devices->Length; )
				{
					indices[i] = devices[i].Index;
				}

				pin_ptr<int> devicePtr = &indices[0];
				CheckError( rtContextSetDevices( mContext, devices->Length, devicePtr ) );
			}
		}

		/// <summary>
		/// Gets the number of devices currently being used by Optix
        /// </summary>
		property unsigned int DeviceCount
		{
			unsigned int get()
			{
				unsigned int count = 0;
				CheckError( rtContextGetDeviceCount( mContext, &count ) );
				return count;
			}
		}
		
		/// <summary>
		/// Gets or Sets the number of CPU threads Optix can use
        /// </summary>
		property int CpuNumThreads
		{
			int get()
			{
				int numThreads = 0;
				CheckError( rtContextGetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_CPU_NUM_THREADS, sizeof( numThreads ), &numThreads ) );

				return numThreads;
			}

			void set( int numThreads )
			{
				pin_ptr<int> ptr = &numThreads;

				CheckError( rtContextSetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_CPU_NUM_THREADS, sizeof( numThreads ), ptr ) );
			}
		}

		/// <summary>
		/// Gets if software paging of device memory has been turned on by the context
        /// </summary>
		property bool GpuPagingActive
		{
			bool get()
			{
				int active = 0;
				CheckError( rtContextGetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_GPU_PAGING_ACTIVE, sizeof( active ), &active ) );

				return active == 1;
			}
		}

		/// <summary>
		/// Gets or Sets the status the user disable option of Optix's GPU paging feature.
        /// </summary>
		property bool GpuPagingDisabled
		{
			bool get()
			{
				int active = 0;
				CheckError( rtContextGetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_GPU_PAGING_FORCED_OFF, sizeof( active ), &active ) );

				return active == 1;
			}

			void set( bool active )
			{
				int forceOff = active ? 0 : 1;
				CheckError( rtContextSetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_GPU_PAGING_FORCED_OFF, sizeof( forceOff ), &forceOff ) );
			}
		}

		/// <summary>
		/// Gets the maximum number of textures handled by OptiX.<br/>
		/// For OptiX versions below 2.5 this value depends on the number of textures supported by CUDA.
        /// </summary>
		property int MaxTextureCount
		{
			int get()
			{
				int count = 0;
				CheckError( rtContextGetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_MAX_TEXTURE_COUNT, sizeof( count ), &count ) );

				return count;
			}
		}

		/// <summary>
		/// Gets the maximum number of textures handled by OptiX.<br/>
		/// For OptiX versions below 2.5 this value depends on the number of textures supported by CUDA.
        /// </summary>
		property Size AvailableMemory
		{
			Size get()
			{
				Size count = 0;
				CheckError( rtContextGetAttribute( mContext, RT_CONTEXT_ATTRIBUTE_AVAILABLE_DEVICE_MEMORY, sizeof( count ), &count ) );

				return count;
			}
		}

		/// <summary>
        /// Gets or Sets the number of ray types for the optix context
        /// </summary>
		property unsigned int RayTypeCount
		{
			unsigned int get()
			{
				unsigned int numRayTypes;
				CheckError( rtContextGetRayTypeCount( mContext, &numRayTypes ) );
				return numRayTypes;
			}

			void set( unsigned int numRays ) 
			{ 
				CheckError( rtContextSetRayTypeCount( mContext, numRays ) ); 
			}
		}

		/// <summary>
        /// Gets or Sets the number of program entries for the optix context
        /// </summary>
		property unsigned int EntryPointCount
		{
			unsigned int get()
			{
				unsigned int numEntryPoints;
				CheckError( rtContextGetEntryPointCount( mContext, &numEntryPoints ) );
				return numEntryPoints;
			}
			void set( unsigned int numEntries ) 
			{ 
				assert( numEntries > 0 ); 
				CheckError( rtContextSetEntryPointCount( mContext, numEntries ) ); 
			}
		}

		/// <summary>
        /// Enables or disables all exceptions
        /// </summary>
		property bool EnableAllExceptions
		{
			void set( bool enable )
			{
				CheckError( rtContextSetExceptionEnabled( mContext, RT_EXCEPTION_ALL, (int)enable ) );
			}
		}

		/// <summary>
        /// Sets the stack size in bytes for the Context
        /// </summary>
		property Size StackSize
		{
			void set( Size size )
			{
				CheckError( rtContextSetStackSize( mContext, size ) );
			}
		}

		/// <summary>
		/// Gets the number of global variables on the Context
        /// </summary>
		property int VariableCount
		{
			virtual int get()
			{
				unsigned int count;
				CheckError( rtContextGetVariableCount( mContext, &count ) );
				return count;
			}
		}

		/// <summary>
		/// Enables or Disables printing from Optix Cuda launch programs
        /// </summary>
		property bool PrintingEnabled
		{
			bool get()
			{
				int enabled = 0;
				CheckError( rtContextGetPrintEnabled( mContext, &enabled ) );

				return enabled == 1;
			}
			void set( bool enabled )
			{
				int on = enabled ? 1 : 0;
				CheckError( rtContextSetPrintEnabled( mContext, on ) );
			}
		}

		/// <summary>
		/// Sets the launch dimension to limit output from rtPrintf to a specific launch index
        /// </summary>
		/// <param name="x">The launch index in the x dimension to which to limit the output of rtPrintf invocations. 
		/// If set to -1, output is generated for all launch indices in the x dimension.</param>
		/// <param name="y">The launch index in the y dimension to which to limit the output of rtPrintf invocations. 
		/// If set to -1, output is generated for all launch indices in the y dimension.</param>
		/// <param name="z">The launch index in the z dimension to which to limit the output of rtPrintf invocations. 
		/// If set to -1, output is generated for all launch indices in the z dimension.</param>
		property OptixDotNet::Math::Int3 PrintLaunchIndex
		{
			OptixDotNet::Math::Int3 get()
			{
				int x, y, z;
				CheckError( rtContextGetPrintLaunchIndex( mContext, &x, &y, &z ) );

				return OptixDotNet::Math::Int3( x, y, z );
			}

			void set( OptixDotNet::Math::Int3 index )
			{
				CheckError( rtContextSetPrintLaunchIndex( mContext, index.X, index.Y, index.Z ) );
			}
		}

		/// <summary>
		/// Gets or Sets the size of the buffer in bytes Optix will use for printing from Cuda programs
        /// </summary>
		property RTsize PrintBufferSize
		{
			RTsize get()
			{
				RTsize size;
				CheckError( rtContextGetPrintBufferSize( mContext, &size ) );
				return size;
			}
			void set( RTsize size )
			{
				CheckError( rtContextSetPrintBufferSize( mContext, size ) );
			}
		}

		/// <summary>
		/// Gets the running state of the Optix context. Returns true if rtContextLaunch is currently active for this Context.
        /// </summary>
		property bool Running
		{
			bool get()
			{
				int running = 0;
				CheckError( rtContextGetRunningState( mContext, &running ) );
				return running == 1;
			}
		}

		/// <summary>
		/// Gets or Sets a global variable on the context with name [s]. If the variable doesn't exist, it is created.
		/// Setting a null value will remove the variable from the context.
        /// </summary>
		property Variable^ default[System::String^]
		{
			virtual Variable^ get ( System::String^ s );
			virtual void set( System::String^s, Variable^ v );
		}

		/// <summary>
		/// Gets or Sets a global variable on the context with name [s]. If the variable doesn't exist, it is created.
		/// Setting a null value will remove the variable from the context.
        /// </summary>
		property Variable^ default[int]
		{
			virtual Variable^ get ( int index );
			virtual void set( int index, Variable^ v );
		}

	internal:
		inline RTcontext InternalPtr() { return mContext; }


	private:
		void CheckError( RTresult result ) {
			if( result != RT_SUCCESS ) {
				const char* message;
				rtContextGetErrorString( mContext, result, &message );

				System::String^ msg = "Optix error: " + this->GetType() + ": ";
				msg += AnsiiToStr( message );
				throw gcnew System::Exception( msg );
			}
		}


	private:
		RTcontext		mContext;

		System::IntPtr	mD3D9Device;
		System::IntPtr	mD3D10Device;
		System::IntPtr	mD3D11Device;
	};
}
